export const name = "rpipeline"

import { Argv, h } from "koishi"

function stringify(x) {
  if (Array.isArray(x)) return x.join("")
  return String(x).replace(/<[/]?(?:null)?>/g, "")
}

export async function apply(ctx) {
  const bindings = new WeakMap()
  const callCounts = new WeakMap()

  const specialUnaryPrefixes = {
    "¿"(session, code, content, argvProps) {
      return specialCommands.WL(session, code, content, argvProps)
    },
    "∪"(session, code, content, argvProps) {
      return specialCommands["∪"](session, code, content, argvProps)
    },
    "∪∪"(session, code, content, argvProps) {
      return specialCommands["∪∪"](session, code, content, argvProps)
    },
    "∪∪∪"(session, code, content, argvProps) {
      return specialCommands["∪∪∪"](session, code, content, argvProps)
    },
    "&lt;|"(session, code, content, argvProps) {
      return specialCommands["|"](session, code, content, argvProps)
    },
  }
  const specialPrefixes = {
    "¿¿"(session, name, arg, content, argvProps) {
      content = stringify(content || "")
      if (content) {
        if (arg) arg += "\f"
        arg += content
      }
      return executeParsed(session, {
        name: "whatcmd",
        args: [name, h.unescape(arg)],
      }, argvProps)
    },
    "^"(session, command, name, content, argvProps) {
      if (command) {
        const arg = stringify(bindings.get(session)?.[name] ?? "")
        return executeSplit(session, command, arg, content, argvProps)
      }
      return Promise.resolve(bindings.get(session)?.[name] ?? "")
    },
  }
  const specialCommands = {
    ECHO(session, arg, content) {
      if (content?.length) return [...h.normalize(arg), ...h.normalize(content)]
      return h.normalize(arg)
    },
    ";"(session, arg, content) {
      if (!specialPrefixes["^"](session, "", "\x7f | SEEN_SEMICOLON_WARNING | \x7f")?.length)
        session.send(session.i18n(".deprecated-semicolon"))
      this["="](session, "\x7f | SEEN_SEMICOLON_WARNING | \x7f", "1")
      return this.ECHO(session, arg, content)
    },
    ":"(session, arg) {
      return arg
    },
    async TR(session, arg, content) {
      return await session.transform(
        h.transform(this.ECHO(session, arg, content), {
          "": (a, c) => c,
          null: (a, c) => c,
          template: (a, c) => c,
        })
      )
    },
    async RUN(session, command, content, argvProps) {
      return await execute(
        session,
        (stringify(command) + " " + stringify(content)).replace(/^\s*\/{0,2}/, ""),
        "",
        argvProps,
      )
    },
    async EVAL(session, code, content, argvProps) {
      code = h("", h.normalize(code)).toString(true)
      if (Array.isArray(content) && content[0]?.type === "rpipeline:expr") {
        const expr = content[0].attrs.data
        if (!code.trim()) code = expr
        else code = `let $=(${expr});\n${code}`
      } else {
        content = h("", h.normalize(content)).toString(true)
        if (!code.trim()) code = content
        else code = `let $=${JSON.stringify(content)};\n${code}`
      }
      return await executeParsed(session, {
        name: "eval",
        args: [code],
      }, argvProps)
    },
    async OPEVAL(session, code, content, argvProps) {
      if (typeof code !== "object" && !code?.trim()) {
        code = h("", h.normalize(content)).toString(true)
      } else {
        code = h("", h.normalize(code)).toString(true)
        content = h.normalize(content)
        code = `{let $=(f=>f(f))(f=>o=>o.map(e=>h(e.type,e.attrs,f(f)(e.children))))(${JSON.stringify(content)});\n${code}}`
      }
      return await executeParsed(session, {
        name: "eval-unsafe",
        args: [code],
      }, argvProps)
    },
    async WL(session, code, content, argvProps) {
      if (typeof code !== "object" && !code?.trim()) {
        code = h.unescape(stringify(content))
      } else {
        code = h.unescape(stringify(code))
        const escaped = h.unescape(stringify(content))
          .replaceAll("\\", "\\\\")
          .replaceAll("\n", "\\n")
          .replaceAll("\r", "\\r")
          .replaceAll("\t", "\\t")
          .replaceAll("\f", "\\f")
          .replaceAll("\v", "\\v")
          .replaceAll('"', '\\"')
        code = `"${escaped}"\n${stringify(code)}`
      }
      return await executeParsed(session, {
        name: "whatlang",
        args: [code],
      }, argvProps)
    },
    async UIUA(session, code, content) {
      const argv = Argv.parse("uiua " + (stringify(code) || ""))
      if (content.length) {
        argv.tokens.splice(1, 0,
          {
            content: "-I",
            quoted: false,
            inters: [],
            terminator: " ",
          },
          {
            content: stringify(content),
            quoted: true,
            inters: [],
            terminator: " ",
          },
        )
      }
      ctx.logger.debug("step* %o", Argv.stringify(argv))
      return await session.execute(argv, true)
    },
    async "∪"(session, code, content, argvProps) {
      return await executeParsed(session, {
        name: "uiua",
        args: [h("", h.normalize(code)).toString(true)],
        options: {
          ...(content?.length && { input: h.normalize(content) }),
        },
      }, argvProps)
    },
    async "∪∪"(session, code, content, argvProps) {
      return await executeParsed(session, {
        name: "uiua",
        args: [h("", h.normalize(code)).toString(true)],
        options: {
          format: true,
          ...(content?.length && { input: h.normalize(content) }),
        },
      }, argvProps)
    },
    async "∪∪∪"(session, code, content, argvProps) {
      return await executeParsed(session, {
        name: "uiua",
        args: [h("", h.normalize(this.ECHO(session, code, content))).toString(true)],
        options: {
          format: "only",
        },
      }, argvProps)
    },
    async RPL(session, code, content, argvProps) {
      return await executeParsed(session, {
        name: "rpipeline",
        args: [h.normalize(code), stringify(content)],
      }, argvProps)
    },
    "="(session, name, content) {
      let o = bindings.get(session)
      if (!o) {
        o = Object.create(null)
        bindings.set(session, o)
      }
      o[stringify(name ?? "").trim()] = content
      return content
    },
    async SPLIT(session, arg, content, argvProps) {
      return await executeParsed(session, {
        name: "split",
        args: [await this.TR(session, arg, content)],
      }, argvProps)
    },
  }
  specialCommands["$"] = specialCommands.TR
  specialCommands["."] = specialCommands.RUN
  specialCommands["&gt;"] = specialCommands.EVAL
  specialCommands["¿"] = specialCommands.WL
  specialCommands["+"] = specialCommands.ECHO
  specialCommands["§"] = specialCommands.SPLIT
  specialCommands["∪"] = specialCommands.UIUA
  specialCommands["|"] = specialCommands.RPL
  specialCommands["&lt;|"] = specialCommands.RPL

  const specialPrefixNames = [
    ...Object.keys(specialUnaryPrefixes),
    ...Object.keys(specialPrefixes)
  ].sort((a, b) => b.length - a.length)

  function execute(session, command, content, argvProps) {
    command = command.trimStart()
    let prefix = specialPrefixNames.find(prefix => command.startsWith(prefix))
    if (prefix && Object.hasOwn(specialUnaryPrefixes, prefix)) {
      command = command.slice(prefix.length)
      ctx.logger.debug("unaryPrefix", [prefix, command, stringify(content)])
      return specialUnaryPrefixes[prefix](
        session,
        command,
        content,
        argvProps,
      )
    }
    const [, commandName, commandArgs] = command.match(/(\S*)(?:\s(.*))?/s)
    return executeSplit(session, commandName, commandArgs || "", content, argvProps)
  }

  function executeSplit(session, commandName, commandArgs, content, argvProps) {
    const contentString = stringify(content)

    let prefix = specialPrefixNames.find(prefix => commandName.startsWith(prefix))
    if (prefix && Object.hasOwn(specialPrefixes, prefix)) {
      commandName = commandName.slice(prefix.length)
      ctx.logger.debug("prefix", [prefix, commandName, commandArgs, contentString])
      return specialPrefixes[prefix](
        session,
        commandName,
        commandArgs,
        content,
        argvProps,
      )
    }

    ctx.logger.debug("step", [commandName, stringify(commandArgs), contentString])

    if (Object.hasOwn(specialCommands, commandName))
      return Promise.resolve(
        specialCommands[commandName](
          session,
          commandArgs,
          content,
          argvProps,
        )
      )

    const argv = Argv.parse(commandName + " " + stringify(commandArgs))
    content &&
      argv.tokens.push({
        content: contentString,
        quoted: true,
        inters: [],
        terminator: "",
      })
    Object.assign(argv, argvProps)
    return session.execute(argv, true)
  }

  function executeParsed(session, argv, argvProps) {
    ctx.logger.debug("parsed", [argv.name, ...[argv.options, argv.args].filter(Boolean)])
    Object.assign(argv, argvProps)
    return session.execute(argv, true)
  }

  ctx
    .command("rpipeline <commands:el>", { strictOptions: true, captureQuote: false })
    .before(({ session }) => {
      const count = callCounts.get(session) ?? 0
      if (count > 20) return session.i18n(".too-many-calls-in-session")
      callCounts.set(session, count + 1)
    })
    .action(({ session, root }, raw, _input) => {
      let commands = raw?.join("") || ""
      let quoteContent = _input || root === true && session.event?.message?.quote?.content || ""
      ctx.logger.debug("pipeline", [commands, quoteContent])

      if (!commands.trim()) return execute(session, quoteContent.trimStart(), "")
      return commands
        .split(/\s\|\s/g)
        .toReversed()
        .reduce(
          async (l, c, i, a) => execute(
            session,
            c.trimStart(),
            await l,
            i === a.length - 1 ? { root: "-lnnbot-pseudo" } : null,
          ),
          quoteContent,
        )
    })

  ctx.middleware((session, next) => {
    if (session.stripped.hasAt && !session.stripped.atSelf) return next()
    const m = session.stripped.content.match(/^&lt;\||^&lt; \|(?:\s|$)/)?.[0] ||
      (session.stripped.appel || session.resolve(ctx.root.config.prefix).includes("")) && session.stripped.content.match(/^\|(?:\s|$)/)?.[0]
    if (m) {
      return session.execute({ name: "rpipeline", args: [h.parse(session.stripped.content.slice(m.length).trimStart())], root: true })
    }
    return next()
  })

  ctx.i18n.define("zh-CN", "commands.rpipeline", {
    description: "指令管道语法糖",
    usage:
      "输入以竖线（不能与非空白字符相邻）分隔的若干条指令，先执行最后一条，将其输出添加到前一条指令末尾（视为单个语法 token，不分词或展开插值，与引用回复时自动添加的隐式参数类似，但不会自动展开 &lt;html&gt; 等消息组件）并执行，依此类推，输出最后的返回值。\n" +
      "此外，在 rpipeline 中一些全大写或符号指令名有特殊效果，包括：\n" +
      "    RUN 或 .：将给定内容当作一条指令执行（自动在字面参数与右侧指令结果〔或引用的内容〕之间添加空格，并重新解析指令语法，包括分词、展开插值）。\n" +
      "    TR 或 $：展开给定内容中的 &lt;html&gt; 等 Koishi 消息组件并剔除 &lt;&gt; &lt;/&gt; 标签。\n" +
      "    ECHO 或 +：将给定内容原封不动地输出，不二次解析指令语法。\n" +
      "    :（半角冒号）：类似于 ECHO，但不包含右侧指令输出的内容。\n" +
      "    =：将右侧指令的结果保存为指定名称的变量，同时原样输出。名称可以是空（称为默认变量）。变量只在同一个会话事件内有效。\n" +
      "    ^：取指定名称变量的内容。\n" +
      "    ^ 后接其他指令名：取指定名称变量的内容，并当作该指令的参数调用。对于右侧指令的结果按该指令原本的方式处理。\n" +
      "    EVAL 或 &gt;：类似于 eval 指令，但会将右侧指令的输出（或引用的内容）存储为字符串变量 $ 而不是视为代码的一部分。\n" +
      "    WL 或 ¿：类似于 whatlang 指令，但会在执行前预先将右侧指令的输出（或引用的内容）以字符串值的形式入栈，而非视为代码的一部分。\n" +
      "    UIUA 或 ∪：类似于 uiua 指令，使用右侧指令的输出作为输入，模拟引用消息时的行为。详见 uiua 的帮助。\n" +
      "    SPLIT 或 §：类似于 split 指令，会自动将输入通过特殊指令 TR 进行展开。\n" +
      "快捷方式：\n" +
      "    &lt; | (commands...)\n" +
      "    &lt;|(commands...)\n" +
      "    | (commands...) （只在可无前缀调用指令的上下文可用）",
    examples:
      "&lt; | imagify | xdi8 —— 希顶转换引用的内容后，输出结果为图片\n" +
      "&lt; | . —— 重新执行引用的指令\n" +
      "&lt; | uniname | &gt; $.slice(8) —— 将引用的内容去掉前 8 个字符后查询其余 Unicode 字符名称\n" +
      "&lt; | ^¿ | : abjad | = —— 将“abjad”作为输入字符串，将引用的内容作为 WhatLang 代码运行",
    messages: {
      "too-many-calls-in-session": "当前会话调用 rpipeline 指令的次数过多…",
      "deprecated-semicolon": "警告：特殊指令 ECHO 的别名“;”已弃用，请改用“+”。",
    },
  })
}
